package org.andengine.opengl.texture.render;

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.opengl.GLES20;

import org.andengine.opengl.exception.GLException;
import org.andengine.opengl.exception.GLFrameBufferException;
import org.andengine.opengl.exception.RenderTextureInitializationException;
import org.andengine.opengl.texture.ITextureStateListener;
import org.andengine.opengl.texture.PixelFormat;
import org.andengine.opengl.texture.Texture;
import org.andengine.opengl.texture.TextureManager;
import org.andengine.opengl.texture.TextureOptions;
import org.andengine.opengl.util.GLHelper;
import org.andengine.opengl.util.GLState;
import org.andengine.util.color.Color;

import java.io.IOException;
import java.nio.IntBuffer;

/**
 * The general workflow with a {@link RenderTexture} is: {@link RenderTexture#init(GLState)} -> {@link RenderTexture#begin(GLState)} -> {@link RenderTexture#end(GLState)} -> {@link RenderTexture#destroy(GLState)}.
 * <p/>
 * (c) Zynga 2011
 *
 * @author Nicolas Gramlich <ngramlich@zynga.com>
 * @since 07:13:05 - 24.08.2011
 */
public class RenderTexture extends Texture {
    // ===========================================================
    // Constants
    // ===========================================================

    private static final int[] VIEWPORT_CONTAINER = new int[4];
    private static final float[] CLEARCOLOR_CONTAINER = new float[4];

    private static final int VIEWPORT_CONTAINER_X_INDEX = 0;
    private static final int VIEWPORT_CONTAINER_Y_INDEX = RenderTexture.VIEWPORT_CONTAINER_X_INDEX + 1;
    private static final int VIEWPORT_CONTAINER_WIDTH_INDEX = RenderTexture.VIEWPORT_CONTAINER_Y_INDEX + 1;
    private static final int VIEWPORT_CONTAINER_HEIGHT_INDEX = RenderTexture.VIEWPORT_CONTAINER_WIDTH_INDEX + 1;

    private static final int CLEARCOLOR_CONTAINER_RED_INDEX = 0;
    private static final int CLEARCOLOR_CONTAINER_GREEN_INDEX = RenderTexture.CLEARCOLOR_CONTAINER_RED_INDEX + 1;
    private static final int CLEARCOLOR_CONTAINER_BLUE_INDEX = RenderTexture.CLEARCOLOR_CONTAINER_GREEN_INDEX + 1;
    private static final int CLEARCOLOR_CONTAINER_ALPHA_INDEX = RenderTexture.CLEARCOLOR_CONTAINER_BLUE_INDEX + 1;

    // ===========================================================
    // Fields
    // ===========================================================

    protected final PixelFormat mPixelFormat;
    protected final int mWidth;
    protected final int mHeight;

    protected int mFramebufferObjectID;
    private int mPreviousFramebufferObjectID;
    private int mPreviousViewPortX;
    private int mPreviousViewPortY;
    private int mPreviousViewPortWidth;
    private int mPreviousViewPortHeight;

    private boolean mInitialized;

    // ===========================================================
    // Constructors
    // ===========================================================

    public RenderTexture(final TextureManager pTextureManager, final int pWidth, final int pHeight) {
        this(pTextureManager, pWidth, pHeight, PixelFormat.RGBA_8888, TextureOptions.NEAREST);
    }

    public RenderTexture(final TextureManager pTextureManager, final int pWidth, final int pHeight, final PixelFormat pPixelFormat) {
        this(pTextureManager, pWidth, pHeight, pPixelFormat, TextureOptions.NEAREST);
    }

    public RenderTexture(final TextureManager pTextureManager, final int pWidth, final int pHeight, final TextureOptions pTextureOptions) {
        this(pTextureManager, pWidth, pHeight, PixelFormat.RGBA_8888, pTextureOptions);
    }

    public RenderTexture(final TextureManager pTextureManager, final int pWidth, final int pHeight, final PixelFormat pPixelFormat, final TextureOptions pTextureOptions) {
        this(pTextureManager, pWidth, pHeight, pPixelFormat, pTextureOptions, null);
    }

    public RenderTexture(final TextureManager pTextureManager, final int pWidth, final int pHeight, final PixelFormat pPixelFormat, final TextureOptions pTextureOptions, final ITextureStateListener pTextureStateListener) {
        super(pTextureManager, pPixelFormat, pTextureOptions, pTextureStateListener);

        this.mWidth = pWidth;
        this.mHeight = pHeight;

        this.mPixelFormat = pPixelFormat;
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    @Override
    public int getWidth() {
        return this.mWidth;
    }

    @Override
    public int getHeight() {
        return this.mHeight;
    }

    public boolean isInitialized() {
        return this.mInitialized;
    }

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    @Override
    protected void writeTextureToHardware(final GLState pGLState) {
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, this.mPixelFormat.getGLInternalFormat(), this.mWidth, this.mHeight, 0, this.mPixelFormat.getGLFormat(), this.mPixelFormat.getGLType(), null);
    }

    // ===========================================================
    // Methods
    // ===========================================================

    /**
     * @param pGLState
     * @throws RenderTextureInitializationException when this {@link RenderTexture} could not be initialized. The {@link GLException} contains the error code. When this exception is throw, all cleanup will be automatically performed through {@link RenderTexture#destroy(GLState)}.
     */
    public void init(final GLState pGLState) throws GLFrameBufferException, GLException {
        this.savePreviousFramebufferObjectID(pGLState);

        try {
            this.loadToHardware(pGLState);
        } catch (final IOException e) {
            /* Can not happen. */
        }

		/* The texture to render to must not be bound. */
        pGLState.bindTexture(0);

		/* Generate FBO. */
        this.mFramebufferObjectID = pGLState.generateFramebuffer();
        pGLState.bindFramebuffer(this.mFramebufferObjectID);

		/* Attach texture to FBO. */
        GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, this.mHardwareTextureID, 0);

        try {
            pGLState.checkFramebufferStatus();
        } catch (final GLException e) {
            this.destroy(pGLState);

            throw new RenderTextureInitializationException(e);
        } finally {
            this.restorePreviousFramebufferObjectID(pGLState);
        }

        this.mInitialized = true;
    }

    /**
     * @see {@link RenderTexture#end(GLState)},
     * {@link RenderTexture#end(GLState, boolean, boolean}}.
     */
    public void begin(final GLState pGLState) {
        this.begin(pGLState, false, false);
    }

    /**
     * @param pColor the {@link Color} to clear this {@link RenderTexture}.
     * @see {@link RenderTexture#end(GLState)},
     * {@link RenderTexture#end(GLState, boolean, boolean}}.
     */
    public void begin(final GLState pGLState, final Color pColor) {
        this.begin(pGLState, pColor.getRed(), pColor.getGreen(), pColor.getBlue(), pColor.getAlpha());
    }

    /**
     * @param pRed   the red portion of the color to clear this {@link RenderTexture}.
     * @param pGreen the green portion of the color to clear this {@link RenderTexture}.
     * @param pBlue  the blue portion of the color to clear this {@link RenderTexture}.
     * @param pAlpha the alpha portion of the color to clear this {@link RenderTexture}.
     * @see {@link RenderTexture#end(GLState)},
     * {@link RenderTexture#end(GLState, boolean, boolean}}.
     */
    public void begin(final GLState pGLState, final float pRed, final float pGreen, final float pBlue, final float pAlpha) {
        this.begin(pGLState, false, false, pRed, pGreen, pBlue, pAlpha);
    }

    /**
     * @param pColor the {@link Color} to clear this {@link RenderTexture}.
     * @see {@link RenderTexture#end(GLState)},
     * {@link RenderTexture#end(GLState, boolean, boolean}}.
     */
    public void begin(final GLState pGLState, final boolean pFlipX, final boolean pFlipY, final Color pColor) {
        this.begin(pGLState, pFlipX, pFlipY, pColor.getRed(), pColor.getGreen(), pColor.getBlue(), pColor.getAlpha());
    }

    /**
     * @param pRed   the red portion of the color to clear this {@link RenderTexture}.
     * @param pGreen the green portion of the color to clear this {@link RenderTexture}.
     * @param pBlue  the blue portion of the color to clear this {@link RenderTexture}.
     * @param pAlpha the alpha portion of the color to clear this {@link RenderTexture}.
     * @see {@link RenderTexture#end(GLState)},
     * {@link RenderTexture#end(GLState, boolean, boolean}}.
     */
    public void begin(final GLState pGLState, final boolean pFlipX, final boolean pFlipY, final float pRed, final float pGreen, final float pBlue, final float pAlpha) {
        this.begin(pGLState, pFlipX, pFlipY);

		/* Save clear color. */
        GLES20.glGetFloatv(GLES20.GL_COLOR_CLEAR_VALUE, RenderTexture.CLEARCOLOR_CONTAINER, 0);

        GLES20.glClearColor(pRed, pGreen, pBlue, pAlpha);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

		/* Restore clear color. */
        GLES20.glClearColor(RenderTexture.CLEARCOLOR_CONTAINER[RenderTexture.CLEARCOLOR_CONTAINER_RED_INDEX], RenderTexture.CLEARCOLOR_CONTAINER[RenderTexture.CLEARCOLOR_CONTAINER_GREEN_INDEX], RenderTexture.CLEARCOLOR_CONTAINER[RenderTexture.CLEARCOLOR_CONTAINER_BLUE_INDEX], RenderTexture.CLEARCOLOR_CONTAINER[RenderTexture.CLEARCOLOR_CONTAINER_ALPHA_INDEX]);
    }

    /**
     * @see {@link RenderTexture#end(GLState)},
     * {@link RenderTexture#end(GLState, boolean, boolean}}.
     */
    public void begin(final GLState pGLState, final boolean pFlipX, final boolean pFlipY) {
        this.savePreviousViewport();
        GLES20.glViewport(0, 0, this.mWidth, this.mHeight);

        pGLState.pushProjectionGLMatrix();

        final float left;
        final float right;
        final float bottom;
        final float top;
        if (pFlipX) {
            left = this.mWidth;
            right = 0;
        } else {
            left = 0;
            right = this.mWidth;
        }
        if (pFlipY) {
            top = this.mHeight;
            bottom = 0;
        } else {
            top = 0;
            bottom = this.mHeight;
        }
        pGLState.orthoProjectionGLMatrixf(left, right, bottom, top, -1, 1);

        this.savePreviousFramebufferObjectID(pGLState);
        pGLState.bindFramebuffer(this.mFramebufferObjectID);

        pGLState.pushModelViewGLMatrix();
        pGLState.loadModelViewGLMatrixIdentity();
    }

    /**
     * @see {@link GLState#flush()}.
     */
    public void flush(final GLState pGLState) {
        pGLState.flush();
    }

    /**
     * @see {@link GLState#finish()}.
     */
    public void finish(final GLState pGLState) {
        pGLState.finish();
    }

    /**
     * @see {@link RenderTexture#begin(GLState)},
     * {@link RenderTexture#begin(GLState, boolean, boolean)},
     * {@link RenderTexture#begin(GLState, Color)},
     * {@link RenderTexture#begin(GLState, float, float, float, float)},
     * {@link RenderTexture#begin(GLState, boolean, boolean, Color)}.
     * {@link RenderTexture#begin(GLState, boolean, boolean, float, float, float, float)}.
     */
    public void end(final GLState pGLState) {
        this.end(pGLState, false, false);
    }

    /**
     * @param pGLState
     * @param pFlush   {@link GLState#flush()} has lower preference than pFinish.
     * @param pFinish  {@link GLState#finish()} had higher preference than pFlush.
     * @see {@link RenderTexture#begin(GLState)},
     * {@link RenderTexture#begin(GLState, boolean, boolean)},
     * {@link RenderTexture#begin(GLState, Color)},
     * {@link RenderTexture#begin(GLState, float, float, float, float)},
     * {@link RenderTexture#begin(GLState, boolean, boolean, Color)}.
     * {@link RenderTexture#begin(GLState, boolean, boolean, float, float, float, float)}.
     */
    public void end(final GLState pGLState, final boolean pFlush, final boolean pFinish) {
        if (pFinish) {
            this.finish(pGLState);
        } else if (pFlush) {
            this.flush(pGLState);
        }

        pGLState.popModelViewGLMatrix();

        this.restorePreviousFramebufferObjectID(pGLState);

        pGLState.popProjectionGLMatrix();

        this.resotorePreviousViewport();
    }

    public void destroy(final GLState pGLState) {
        this.unloadFromHardware(pGLState);

        pGLState.deleteFramebuffer(this.mFramebufferObjectID);

        this.mInitialized = false;
    }

    protected void savePreviousFramebufferObjectID(final GLState pGLState) {
        this.mPreviousFramebufferObjectID = pGLState.getActiveFramebuffer();
    }

    protected void restorePreviousFramebufferObjectID(final GLState pGLState) {
        pGLState.bindFramebuffer(this.mPreviousFramebufferObjectID);
    }

    protected void savePreviousViewport() {
        GLES20.glGetIntegerv(GLES20.GL_VIEWPORT, RenderTexture.VIEWPORT_CONTAINER, 0);

        this.mPreviousViewPortX = RenderTexture.VIEWPORT_CONTAINER[RenderTexture.VIEWPORT_CONTAINER_X_INDEX];
        this.mPreviousViewPortY = RenderTexture.VIEWPORT_CONTAINER[RenderTexture.VIEWPORT_CONTAINER_Y_INDEX];
        this.mPreviousViewPortWidth = RenderTexture.VIEWPORT_CONTAINER[RenderTexture.VIEWPORT_CONTAINER_WIDTH_INDEX];
        this.mPreviousViewPortHeight = RenderTexture.VIEWPORT_CONTAINER[RenderTexture.VIEWPORT_CONTAINER_HEIGHT_INDEX];
    }

    protected void resotorePreviousViewport() {
        GLES20.glViewport(this.mPreviousViewPortX, this.mPreviousViewPortY, this.mPreviousViewPortWidth, this.mPreviousViewPortHeight);
    }

    public int[] getPixelsARGB_8888(final GLState pGLState) {
        return this.getPixelsARGB_8888(pGLState, 0, 0, this.mWidth, this.mHeight);
    }

    public int[] getPixelsARGB_8888(final GLState pGLState, final int pX, final int pY, final int pWidth, final int pHeight) {
        final int[] pixelsRGBA_8888 = new int[pWidth * pHeight];
        final IntBuffer glPixelBuffer = IntBuffer.wrap(pixelsRGBA_8888);
        glPixelBuffer.position(0);

        this.begin(pGLState);
        GLES20.glReadPixels(pX, pY, pWidth, pHeight, this.mPixelFormat.getGLFormat(), this.mPixelFormat.getGLType(), glPixelBuffer);
        this.end(pGLState);

        return GLHelper.convertRGBA_8888toARGB_8888(pixelsRGBA_8888);
    }

    public Bitmap getBitmap(final GLState pGLState) {
        return this.getBitmap(pGLState, 0, 0, this.mWidth, this.mHeight);
    }

    public Bitmap getBitmap(final GLState pGLState, final int pX, final int pY, final int pWidth, final int pHeight) {
        if (this.mPixelFormat != PixelFormat.RGBA_8888) {
            throw new IllegalStateException("Currently only 'PixelFormat." + PixelFormat.RGBA_8888 + "' is supported to be retrieved as a Bitmap.");
        }

        return Bitmap.createBitmap(this.getPixelsARGB_8888(pGLState, pX, pY, pWidth, pHeight), pWidth, pHeight, Config.ARGB_8888);
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================
}
